package hydrap2p.helper;

import hydrap2p.ServiceLocation;
import hydrap2p.servicemanager.ServiceManager;
import hydrap2p.servicemanager.NameTaken;

import java.io.IOException;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.Random;

import org.apache.thrift.TException;
import org.apache.thrift.TProcessor;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;

public class ThriftServerThread extends Thread {
	
	private static final int PORTTRIES=50; 

	private ServerSocket socket;
	private TServerTransport servertransport;
	private final TThreadPoolServer server;
	private final String name;
	private final ServiceLocation location;
	
	public ThriftServerThread(final TProcessor serverType, final String name) throws BindException, IOException, NameTaken
	{
		this(serverType, name, -1);
	}
	/**
	 * Creates an unstarted Thread that acts as a Thrift server
	 * @param serverType	The Thrift Processor object that should be listening
	 * @param name			The name that should be registered in the Manager for this server
	 * @param defaultPort	The default port that will be tried first. It will try up to 50 (PORTTRIES) other ports if the default port is unavailable
	 * @throws BindException	Thrown if no open ports could be found within the 50 tries
	 * @throws IOException		Thrown if the manager could not be contacted
	 * @throws NameTaken		Thrown if the given name is already registered in the Manager
	 */
	public ThriftServerThread(final TProcessor serverType, final String name, final int defaultPort) throws BindException, IOException, NameTaken
	{
		// Bind to a preferred TCP port, or fallback to some random one
		Random ports = new Random();
		socket = new ServerSocket();
		int attempts = PORTTRIES;
		int minport = 2000;
		int maxport = 65000;
		int port = defaultPort;
		
		this.name=name;

		while(socket.getLocalPort() == -1 && attempts > 0) {
			if (port>=0)
			{
				try {
					socket.bind(new InetSocketAddress("localhost", port));
				} catch(IOException e) {}
			}
			
			if(socket.getLocalPort() == -1)
				port = ports.nextInt(maxport - minport) + minport;
			attempts--;
		}

		if(socket.getLocalPort() == -1)
			throw new BindException();

		location = new ServiceLocation("localhost", port);
		
		servertransport = new TServerSocket(socket);
		TThreadPoolServer.Args args = new TThreadPoolServer.Args(servertransport);
		args.processor(serverType);
		server = new TThreadPoolServer(args);
		
		
		// Notify manager of our location, now that we're listening at it
		if (name!=null)
		{
			try {
				ServiceManager.Client manager = ServiceConnector.getServiceManager();
				if (manager==null)
					throw new TException();
				manager.setServiceLocation(name, location);
			} catch(NameTaken e) {
				throw e;
			} catch(TException e) {
				throw new IOException("Failed to contact Manager");
			}
		}
		
		if (name!=null)
			setName("ThriftServerThread - "+name+" "+location);
		else
			setName("ThriftServerThread - "+location);
	}
	
	/**
	 * Returns a copy of the ServiceLocation
	 * @return a copy of the ServiceLocation
	 */
	public ServiceLocation getLocation()
	{
		return new ServiceLocation(location);
	}
	
	/**
	 * Starts running the thread
	 */
	@Override
	public void run() {
		server.serve();
	}
	
	/**
	 * Stops the server nicely
	 * Also tries to unregister from the Manager
	 */
	public void shutdown() {
		ServiceManager.Client manager;
		try {
			manager = ServiceConnector.getServiceManager();
			if (manager!=null && name!=null)
				manager.delServiceLocation(name);
		} catch (TException e) {
			// Failed to notify the manager that we're shutting down, oh noes
		}
		server.stop();
		try {
			if (servertransport!=null)
				servertransport.close();
		} catch (Exception e) {
			// Should be closed now
		}
		try {
			if (socket!=null)
				socket.close();
		} catch (IOException e) {
			// Should be closed now
		}
	}
}
